package system.communication;

import system.track.Direction;
import system.track.Sensor;
import system.track.SwitchAngle;
import java.io.IOException;
import java.io.PrintStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.LinkedList;

/**
 * The ConcreteMonitor implements the Monitor interface and uses the Singleton 
 * object-oriented pattern. All Messages to the train are handled with the Monitor.
 * The MessageListener places all Reply messages and Sensor trip messages into 
 * the Monitor's respective lists and the Monitor will get the Messages out of its
 * list to return specified information based on Input. The ConcreteMonitor must
 * wait for the MessageListener to retrieve a reply from the Train before sending 
 * the next message and thus is put on wait after printing to the socket. The 
 * MessageListener will notify the ConcreteMonitor to wake after it receives 
 * and pushes the Message to the Monitor. 
 * 
 * To use the ConcreteMonitor you must use the getInstance method in order to call
 * other methods within the ConcreteMonitor, this is per a Singleton pattern. All
 * static class variables are accessed in the normal way. The ConcreteMonitor also
 * synchronizes all methods that write input to the socket in order to properly 
 * queue by access time all requests.
 * 
 * @author Elliot Cash
 * @version Cycle 1 - complete
 */
public class ConcreteMonitor implements Monitor
{
    private final int BLOCK_RANGE_MAX = 9; //Constants defined for ease in conditionals.
    private final int BLOCK_RANGE_MIN = 1;
    private final int SWITCH_RANGE_MAX = 6;
    private final int SWITCH_RANGE_MIN = 1;
    private final int SPEED_MIN = 0;
    private final int SPEED_MAX = 7;
    private final int LED_MIN = 0;
    private final int LED_MAX = 255;
    public static ConcreteMonitor instance; // Singleton pattern, only ONE ConcreteMonitor may exist.
    public static volatile boolean waiting; // Shows if the ConcreteMonitor is "waiting" for a reply.
    private MessageListener mlistener;
    public LinkedList<Sensor> sensors; // The Sensors registered to the Monitor.
    public Socket s; // The Socket the ConcreteMonitor communicates with.
    public PrintStream osw; // The PrintStream object that writes input to the socket.
    public BufferedReader br; // The BufferedReader which receives output from the socket.
    public static LinkedList<Message> sensorMessages,  // Lists of respective Message types.
        blockMessages, switchMessages, handControlMessages, 
        ledMessages, otherMessages;
    
    /**
     * Gets the only instance of the ConcreteMonitor.
     * 
     * @return the instance variable which calls the private constructor within the
     * ConcreteMonitor.
     */
    public static ConcreteMonitor getInstance()
    {
        if(instance == null) //if the Monitor has not been instantiated
        {
            instance = new ConcreteMonitor(); //create a new one
        }
        return instance; //return either the already-created Monitor or the new one
    }

    /**
     * Constructor for Objects of class ConcreteMonitor.  This Constructor is
     * only called once at the beginning of the execution of the train System;
     * otherwise the instance of the ConcreteMonitor is evoked from the 
     * getInstance method.
     */
    private ConcreteMonitor()
    {
        try
        {
            //set up the Socket; in this case it's set to connect to the simulator
            s = new Socket("localhost", 6543);
            //the output stream - data sent to the system
            osw = new PrintStream(s.getOutputStream(),true);
            //the input stream - data gotten from the system
            br = new BufferedReader(new InputStreamReader(s.getInputStream()));
            //setting up storage for all messages
            sensors = new LinkedList<Sensor>();
            sensorMessages = new LinkedList<Message>();
            blockMessages = new LinkedList<Message>();
            switchMessages = new LinkedList<Message>();
            handControlMessages = new LinkedList<Message>();
            ledMessages = new LinkedList<Message>();
            otherMessages = new LinkedList<Message>();
            //start the Listener, using the input stream set up above
            startListener();
        }
        catch(UnknownHostException uhe)
        {
            System.err.println(uhe.getMessage());
        }
        catch(IOException ioe)
        {
            System.err.println(ioe.getMessage());
        }
    }

    /**
     * Ends processes for the Monitor; shuts down the listener and closes the 
     * Socket.
     * 
     * @throws Throwable
     */
    protected void finalize()
        throws Throwable
    {
        mlistener.end();
        s.close();
    }
    
    /**
     * Sets direction and speed attributes to a block on the track.
     * 
     * @param id an ID corresponding to the blocks on the track with a range of 1-9.
     * @param direction one of the instantiated object of the Direction enum in the track package.
     * @param speed a speed set to the specific block with a range of 0-7.
     */
    public synchronized void setBlock(int id, Direction direction, int speed)
    {
        //check to make sure it's a valid Block ID
        if(id >= BLOCK_RANGE_MIN && id <= BLOCK_RANGE_MAX)
        {
            //check to make sure the speed is valid
            if(speed >= SPEED_MIN && speed <= SPEED_MAX)
            {
                 switch(direction)
                 {
                     case CLOCKWISE:
                         osw.println("*B" + id + "{CW," + speed + "}");
                         waitForResponse(); //MOK must be returned
                         break;
                     case COUNTERCLOCKWISE:
                         osw.println("*B" + id + "{CCW," + speed + "}");
                         waitForResponse(); //MOK must be returned
                         break;
                     default:
                         System.err.println("Bad direction arg.");
                 }
            }
            else
            {
                System.err.println("Bad speed specified.");
            }
        }
        else
        {
            System.err.println("Bad block specified.");
        }
    }

    /**
     * Retrieves a block's Direction.
     * 
     * @param id an ID corresponding to the blocks on the track with a range of 1-9.
     * @return the Direction of the Block corresponding to the id input.
     */
    public synchronized Direction getBlockDirection(int id)
    {
        //the default value is CCW
        Direction returnValue = Direction.COUNTERCLOCKWISE;
        if(id >= BLOCK_RANGE_MIN && id <= BLOCK_RANGE_MAX)
        {
            osw.println("*B" + id + "{?}"); //query the Block
            waitForResponse(); //get the Block's reply
            if(blockMessages.removeLast().getArg(MessageArg.BLOCK_DIRECTION).equals("CW"))
            {
                returnValue = Direction.CLOCKWISE;
            }
        }
        else
        {
            System.err.println("Bad block specified.");
        }
        return returnValue;
    }

    /**
     * Retreives the current speed set on a block.
     * 
     * @param id an ID corresponding to the blocks on the track with a range of 1-9.
     * @return an int representing the current set speed of the block.
     */
    public synchronized int getBlockSpeed(int id)
    {
        int returnValue = -1;
        if(id >= BLOCK_RANGE_MIN && id <= BLOCK_RANGE_MAX )
        {
            osw.println("*B" + id + "{?}"); //query the Block
            waitForResponse(); //get the Block's reply
            returnValue = Integer.parseInt(blockMessages.removeLast().getArg(MessageArg.BLOCK_SPEED));
        }
        else
        {
            System.err.println("Bad block specified.");
        }
        return returnValue;
    }

    /**
     * Sets a switch on the track to specified SwitchAngle.
     * 
     * @param id an ID corresponding to the switches on the track with a range of 1-6.
     * @param switchAngle one of the instantiated object of the SwitchAngle enum in the track package.
     */
    public synchronized void setSwitch(int id, SwitchAngle switchAngle)
    {
        //check for valid Switch ID
        if(id >= SWITCH_RANGE_MIN && id <= SWITCH_RANGE_MAX)
        {
            switch(switchAngle)
            {
                case NINETY_DEGREES:
                    osw.println("*T" + id + "{90}"); //send the angle change
                    waitForResponse(); //confirm
                    break;
                case ONE_EIGHTY_DEGREES:
                    osw.println("*T" + id + "{180}"); //send the angle change
                    waitForResponse(); //confirm
                    break;
                default:
                    System.err.println("Bad angle specified.");
            }
        }
        else
        {
            System.err.println("Bad switch specified.");
        }
    }

    /**
     * Retrieves a switch's current angle.
     * 
     * @param id an ID corresponding to the switches on the track with a range of 1-6.
     * @return a SwitchAngle corresponding to the id input.
     */
    public synchronized SwitchAngle getSwitchAngle(int id)
    {
        //the default is 180
        SwitchAngle returnValue = SwitchAngle.ONE_EIGHTY_DEGREES;
        //check for valid Switch ID
        if(id >= SWITCH_RANGE_MIN && id <= SWITCH_RANGE_MAX)
        {
            osw.println("*T" + id + "{?}"); //query the Switch
            waitForResponse(); //get the Switch's response
            if(switchMessages.removeLast().getArg(MessageArg.SWITCH_ANGLE).equals("90"))
            {
                returnValue = SwitchAngle.NINETY_DEGREES;
            }
        }
        else
        {
            System.err.println("Bad switch specified.");
        }
        return returnValue;
    }

    /**
     * Toggles a Switch.
     * 
     * @param id an ID corresponding to the switches on the track with a range of 1-6.
     */
    public synchronized void toggleSwitch(int id)
    {
        //check for valid Switch ID
        if(id >= SWITCH_RANGE_MIN && id <= SWITCH_RANGE_MAX)
        {
            osw.println("*T" + id + "{T}"); //send the change command to the Switch
            waitForResponse(); //confirm
        }
        else
        {
            System.err.println("Bad switch specified.");
        }
    }

    /**
     * Sets the physical LED lights (There exist 8 individual lights on the LED) 
     * 
     * @param value an integer with a range of 0-255.
     * <table border=1><caption><i>Examples</i></caption><tr><td>Value</td><td>Binary Rep</td><td>On Lights</td></tr>
     * <tr><td>0</td><td>00000000</td><td>None</td></tr>
     * <tr><td>1</td><td>00000001</td><td>First</td></tr>
     * <tr><td>2</td><td>00000010</td><td>Second</td></tr>
     * <tr><td>5</td><td>00000101</td><td>First, Third</td></tr>
     * <tr><td>8</td><td>00001000</td><td>Fourth</td></tr>
     * <tr><td>48</td><td>00110000</td><td>Fifth, Sixth</td></tr>
     */
    public synchronized void setLED(int value)
    {
        //check for valid LED value
        if(value >= LED_MIN && value <= LED_MAX)
        {
            osw.println("*L{" + value + "}"); //send the LED value
            waitForResponse(); //confirm
        }
        else
        {
            System.err.println("Bad value specified.");
        }
    }

    /**
     * Gets the value of the LED
     * 
     * @return an integer representing the value of the LED.
     */
    public synchronized int getLED()
    {
        int returnValue;
        osw.println("*L{?}"); //query the LED
        waitForResponse(); //get the LED's response
        returnValue = Integer.parseInt(ledMessages.removeLast().getArg(MessageArg.LED_VALUE));
        return returnValue;
    }

    /**
     * This tells the monitor to send out for a HandControl message
     * 
     * @param id an integer representing the id of the HandControl
     */
    public synchronized void getHandControlStatus(int id)
    {
        //there are two Hand Controls
        if(id == 1 || id == 2)
        {
            osw.println("*U" + id + "{?}"); //query the HC
            waitForResponse(); //get the HC's response
        }
        else
        {
            System.err.println("Bad HandController id specified");
        }
    }

    /**
     * Stops the train
     */
    public synchronized void stop()
    {
        osw.println("*M{STOP}"); //send the stop command
        waitForResponse(); //confirm
    }

    /**
     * Registers a sensor to the Monitor.  Sensors are stored in groups, so that
     * only certain Sensor messages will be shown to the user.
     *
     * @param sensor A Sensor object to be registered to the Monitor.
     */
    public synchronized void registerSensor(Sensor sensor)
    {
        sensors.add(sensor);
    }

    /**
     * Tests to see if there are any sensor Messages in the Monitor.
     *
     * @return a boolean value. Returns true if the Monitor contains sensor
     * Messages, and returns false if it is empty.
     */
    public synchronized boolean hasSensorMessage()
    {
        return !sensorMessages.isEmpty();
    }

    /**
     * Retrieves the most current sensor Message in the Monitor.
     *
     * @return the last sensor Message retreived by the Listener.
     */
    public synchronized Message getSensorMessage()
    {
        return sensorMessages.removeLast();
    }

    /**
     * Starts the MessageListener class; creates a Listener and starts it.
     */
    public synchronized void startListener()
    {
        this.mlistener = new MessageListener(br);
        mlistener.setName("listener");
        mlistener.start();
    }
    
    /**
     * Halt's the Monitor's thread in order to wait for a response from the
     * MessageListener.
     */
    private void waitForResponse()
    {
        try
        {
            waiting = true;
            while(waiting)
            {
                Thread.sleep(2);
            }
        }
        catch(InterruptedException ex)
        {
            System.err.println(ex.getMessage());
        }
    }
    
    /**
     * Notifies the Monitor that a reply has been received and unhalts the Thread.
     */
    public synchronized void notifyForResponse()
    {
        waiting = false;
    }
}